ReplicationGraph应用

简介

UnrealEngine 基本的网络同步在进行同步时需要对每个连接判断每个 Actor 是否需要同步,开销较大;

可以通过实现 Replication ,实现一个不同的 ReplicationDriver 来优化性能;

ReplicationWorld 分为多个区域 Grid,把 Actor 根据所在 Grid 进行分类编组,可以快速找出需要同步的 Actor,同时确定哪些区域需要进行同步复制。

基本原理

image-20240807154320652

对于需要按照空间划分(Spatialize)的 Actor 来说:

首先将 World 按照 CellSize 抽象成一个个 Cell,一个 Cell 对应一个 GridNode,这里的 GridNode 只会在具体用到的时候再创建;

那么对于一个 Actor,根据 LocationCellSizeSpatialBias,就可以计算出其所在的具体 Cell,即 GetCellInfoForActor

构建

构建 ReplicationGraph 时,在 GridNode 中记录 Actor

对于 Actor,就可以根据 CellInfo、以及 CullDistance 找到需要同步该 ActorCell,这里 Cell 对应的 Grid 中会把该 Actor 记录下来;

也就是上述的粉色区域部分,每个 Grid 都会记录下该 Actor

对于 Spatialize_Static,即静态物体,显然只需要 Add 时计算一次即可;

对于 Spatialize_Dynamic,即动态物体,会在每次开始 PrepareForReplication 时,根据 DynamicActorPreviousCellInfoNewCellInfo,更新对应的 Grid 存储信息;

查询

在对于一个 Connection 查询哪些 Actor 需要同步时:

UpdateGatherLocationsForConnection 更新 Connection 对应的 Location

根据该 Location 计算出该 Connection 对应的 Cell,把 Cell 对应的 GridNode 中的 Actors 同步下去即可;

源码浅析

数据结构

classDiagram
    class UReplicationGraph
  1. FGlobalActorReplicationInfo:同步的 Actor 信息,包括

SettingsFClassReplicationInfo,每个 Actor Class 对应的同步配置,包括 CullDistanceReplicationPeriodFramePriorityScale 等;

EventsFGlobalActorReplicationEvents Events,记录休眠状态的改变时,从休眠列表中添加或者移除;

FConnectionReplicationActorInfo:对于 Connection 同步的 Actor 信息;

  1. ReplicationGraphNodeGraphNode 基类;

  2. ReplicationGraphNode_ActorList:记录同步的 Actors;在 StreamingLevelCollection (以 SubLevelNameKeyList)中记录 SubLevelActor,否则在 ReplicationActorList 中记录;

  3. GlobalGraphNodes:维护 GridSpatialization2D

GraphNode

classDiagram
    class UReplicationGraphNode {
        TArray~UReplicationGraphNode*~ AllChildNodes
        GatherActorListsForConnection()
        RouteAddNetworkActorToNodes()
        RouteRemoveNetworkActorToNodes()
    }	

    UReplicationGraphNode <|-- UReplicationGraphNode_ActorList 
    class UReplicationGraphNode_ActorList {
    	FActorRepListRefView ReplicationActorList
    }
        
    UReplicationGraphNode <|-- UReplicationGraphNode_GridSpatialization2D
    class UReplicationGraphNode_GridSpatialization2D {
        TArray~TArray[UReplicationGraphNode_GridCell*]~ Grid;
    }
    
    UReplicationGraphNode <|-- UReplicationGraphNode_AlwaysRelevant
    class UReplicationGraphNode_AlwaysRelevant{
        TArray~UClass*~AlwaysRelevantClasses;
    }
    
    UReplicationGraphNode <|-- UReplicationGraphNode_ActorListFrequencyBuckets 
    
    UReplicationGraphNode_ActorList <|-- UReplicationGraphNode_GridCell
    
    UReplicationGraphNode_ActorList <|-- UReplicationGraphNode_AlwaysRelevant_ForConnection
  1. GridSpatalization2D:将世界划分为 2D 网格,按位置把 Actor 分到不同的 GridCell 中,按空间管理Actor 是否进行同步,每帧更新 GridCell 内的 Actor
  2. GridCellReplicationActorList 缓存着在该 GridCell 中的所有静态 ActorDynamicNodes 里记录动态的 ActorDormancyNode 里记录休眠的Actor
  3. AlwaysRelevant :处理总是发送 Net Updates 给 所有 ConnectionsActors
  4. AlwaysRelevant_ForConnection:处理总是发送 Net Updates 给 特定 ConnectionActors ,一般是同步给 PlayerControllerViewTarget
  5. ActorListFrequencyBuckets :记录地图格子上的 动态 Actor ;

生命周期

Init

graph TD

Start(UNetDriver::InitBase) --> A("UNetDriver::SetReplicationDriver")
A --> B("UReplicationGraph::InitializeActorsInWorld")
B --> | 将World中的同步对象添加到对应GraphNode | C("UReplicationGraph::InitializeForWorld")
C --> D("UReplicationGraph::AddNetworkActor(AActor* Actor)") 

A --> E(InitForNetDriver)
E --> F(InitGlobalActorClassSettings)
E --> G(InitGlobalGraphNodes)

InitGlobalActorClassSettings:设置 CulltDistanceReplicationPeriodFrame; 等信息注册 Actor 对应的 ClassReplicationInfoGlobalActorReplicationInfoMap

InitGlobalGraphNodes:创建 GridSpatialization2DAlwaysRelevantGraphNode

InitConnectionGraphNodes:创建 AlwaysRelevantForConnectionGraphNode

RouteAddNetworkActorToNodes :生成 Actor 时,添加 NetworkActor,分发 ActorGraphNode

RouteRemoveNetworkActorToNodes :销毁 Actor 时,删除 NetworkActor,通知GraphNode 移除 Actor

Repliate

graph TB
Start(ServerReplicateActors) --> A(PrepareForReplication)
A --> B(GatherActorListsForConnection)
B --> C(ReplicateActorListsForConnections_Default)
C --> D[ReplicateSingleActor]
  1. PrepareForReplication : 调用 GraphNodePrepareForReplication

    对于 GridSpatialization2D,会遍历 DynamicActor,更新其所在的 Grid

    对于 AlwaysRelevant,记录需要同步给所有连接的 Actor

  2. GatherActorListsForConnection : 遍历 Connections 收集 ReplicationActorList

    针对每个 Connection 遍历 GlobalGraphNodesConnectionConnectionGraphNodes,调用其 GatherActorListsForConnection,收集需要同步给这个ConnectionActor

    对于 GridSpatialization2D,通过其 GridCellNode 根据 ActorViewLocation 收集;

    收集的 Actor 默认加到 GatheredReplicationListsForConnection 里的 EActorRepListTypeFlags.Default List

  3. ReplicateActorListsForConnections_Default:进行 Replicate 的同步检测与排序,对 GatheredReplicationListsForConnection 里的 Actor,进行检测;

    首先排除 Dormancy、不满足ReplicateFrameActor,然后根据优先级排序(DistanceStarvation逻辑判定Owner & ViewTarget),将结果缓存在 PrioritizedReplicationList 中;

  4. ReplicateSingleActor:对排好序的 PrioritizedReplicationList 调用 ReplicateSingleActor 进行同步,将对象属性序列化到流中;

具体应用

业务划分

可以按照这些逻辑区分各个 Actor

  1. Node_AlwaysRelevant_ForConnection:一直同步的 Actor,比如 GameState、全局同步对象;
  2. Node_GridSpatialization2D:用 2D Grid 划分,按照区域同步,比如 PawnCharacterSceneItem 等;
  3. Node_PlayerStateFrequencyLimiter:用于 PlayerState,控制同步频率;

初始化

在一个尽早的合适的地方注册UReplicationDriver::CreateReplicationDriverDelegate()

LyraULyraReplicationGraph 的构造函数中(创建 CDO 时)进行;

后续 UNetDriver::InitBase -> SetReplicationDriver 时,会使用该方法进行自定义 ReplicationGraph 的注册:

1
2
3
4
5
6
7
8
9
UReplicationDriver* UReplicationDriver::CreateReplicationDriver(UNetDriver* NetDriver, const FURL& URL, UWorld* World)
{
if (CreateReplicationDriverDelegate().IsBound())
{
return CreateReplicationDriverDelegate().Execute(NetDriver, URL, World);
}

// ...
}

配置化

可以参考 Lyra 注册 ULyraReplicationGraphSettings : UDeveloperSettingsBackedByCVarsProject Settings 中,也可以自定义一个 Blueprint 用于记录所有相关配置(在初始化时,重载为初始化该 BP);

InitGlobalActorClassSettings

进行 Class 相关设置:

  1. SetPolicy

记录 TClassMap<EClassRepNodeMapping> ClassRepNodePolicies 用于表示 Class 对应的 Node Policy

RouteAddNetworkActorToNodesRouteRemoveNetworkActorToNodes 时,通过 GetMappingPolicy(UClass* Class) 找到对应 PolicyNode 对应方法;

1
2
3
EClassRepNodeMapping* PolicyPtr = ClassRepNodePolicies.Get(Class);
EClassRepNodeMapping Policy = PolicyPtr ? *PolicyPtr : EClassRepNodeMapping::NotRouted;
return Policy;

可以提供自定义 Policy 的配置,同时需要通过 Class 的一些基础配置设置好 Policy,比如 bAlwaysRelevant && !bOnlyRelevantToOwner 显然是 RelevantAllConnections

  1. SetClassInfo

FGlobalActorReplicationInfoMap GlobalActorReplicationInfoMap 记录着所有 Class 对应的同步数据,

通过将自定义数据 GlobalActorReplicationInfoMap.SetClassInfo(Class, Info),并记录 ExplicitlySetClasses 表示该 Class 为自定义类型,防止后续被重载;

可以提供配置化的 Class->FClassReplicationInfo Map

InitGlobalGraphNodes

进行 ReplicationNode 相关设置,CreateNode 并进行参数设置:

  1. 设置 GridNodeCellSize(每个 Grid 的大小)、SpatialBias(边界值,ActorLocation 超出时会调用 HandleActorOutOfSpatialBounds 设置 bNeedReubild 并重构 Grid),是否 EnableSpatialRebuilds(决定 AddToClassRebuildDenyList);
  2. 设置 PlayerStateNodeTargetActorsPerFrame

同时可以进行一些额外的参数设置,比如自定义了上层同步频率等 ;

同步频率控制

控制同步频率和总量;

ClassLimit

进行 ReplicateActorListsForConnections_Default,在 ReplicateActorsForConnection 时,可以针对 RepItem.Actor 的类型进行限制,直接限制该次同步该 ActorClass 可同步的总数量;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool IsClassLimitExceeded(AActor* Actor)
{
if (!IsValid(Actor)) return false;
auto CountPtr = ClassLimitMap.Find( Actor->GetClass() );
if (CountPtr == nullptr) return true;
return (*CountPtr)-- > 0;
}

void ReplicateActorsForConnection(UNetConnection* NetConnection, FPerConnectionActorInfoMap& ConnectionActorInfoMap, UNetReplicationGraphConnection* ConnectionManager, const uint32 FrameNum)
{
// ...

ResetClassLimit(); // 重置可同步总量
for (int32 ActorIdx = 0; ActorIdx < PrioritizedReplicationList.Items.Num(); ++ActorIdx)
{
const FPrioritizedRepList::FItem& RepItem = PrioritizedReplicationList.Items[ActorIdx];
AActor* Actor = RepItem.Actor;
// ...
if (ActorInfo.Channel && IsClassLimitExceeded(Actor) /* 限制同步数量 */)
{
continue;
}
}
}

Node_PlayerStateFrequencyLimiter

引入 Node_PlayerStateFrequencyLimiter 用于控制 PlayerState 的同步频率,本质是对某一类 Class(可能是不被 AOI 管理,需要一直同步的),进行同步降频;

维护 TArray<FActorRepListRefView> ReplicationActorLists,每个 FActorRepListRefView 中记录最多 TargetActorsPerFrameActor,在同步时,对 ReplicationActorLists[ Params.ReplicationFrameNum % ReplicationActorLists.Num() ] 进行同步(帧号 % ListNum);

需要特别注意:当 NotifyAddNetworkActor 时,进行 ReplicationActorLists 的扩张,当 NotifyRemoveNetworkActor 时,也需要补充上空位(可以将最后的 Actor,补充到被删去的 Actor 的位置);

同时,新增的 Actor 应该在 ForceNetUpdateReplicationActorList 中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
void UReplicationGraphNode_PlayerStateFrequencyLimiter::NotifyAddNetworkActor(const FNewReplicatedActorInfo& ActorInfo)
{
if (ReplicationActorLists.IsEmpty())
ReplicationActorLists.AddDefaulted();

FActorRepListRefView* CurrentList = &ReplicationActorLists.Last();
if (CurrentList->Num() >= TargetActorsPerFrame)
{
ReplicationActorLists.AddDefaulted();
CurrentList = &ReplicationActorLists.Last();
}

CurrentList->Add(ActorInfo.Actor);
ForceNetUpdateReplicationActorList.Add(ActorInfo.Actor);
}

bool UReplicationGraphNode_PlayerStateFrequencyLimiter::NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound)
{
bool bRemovedSomething = false;
int RemoveListIndex = 0;

for (int Index = 0; Index < ReplicationActorLists.Num(); Index++)
{
if (List[Index].Remove(ActorInfo.Actor))
{
bRemovedSomething = true;
RemoveListIndex = Index;

if (ForceNetUpdateReplicationActorList.Num())
{
ForceNetUpdateReplicationActorList.RemoveFast(ActorInfo.Actor);
}

break;
}
}

if (!bRemovedSomthing) return false;

// 将 LastList 的第一个 Actor 移到 RemoveListIndex 对应的 List
if (ReplicationActorLists.Num() > 1 && RemoveListIndex < ReplicationActorLists.Num())
{
FActorRepListRefView* CurrentList = &ReplicationActorLists.Last();

if (!CurrentList->IsEmpty())
{
auto Actor = (*CurrentList)[0];
ReplicationActorLists[RemoveListIndex].Add(Actor);
CurrentList->RemoveAtSwap(0);
}

if (CurrentList->IsEmpty())
{
ReplicationActorLists.RemoveAt(ReplicationActorLists.Num() - 1);
}
}

return true;
}

void UReplicationGraphNode_PlayerStateFrequencyLimiter::NotifyResetAllNetworkActors()
{
Super::NotifyResetAllNetworkActors();

for (auto& List : ReplicationActorLists)
{
List.Reset();
}

ReplicationActorLists.Reset();
ForceNetUpdateReplicationActorList.Reset();
}

void UReplicationGraphNode_PlayerStateFrequencyLimiter::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params)
{
if (ReplicationActorLists.Num() > 0)
{
int ListIdx = Params.ReplicationFrameNum % ReplicationActorLists.Num();
Params.OutGatheredReplicationLists.AddReplicationActorList(ReplicationActorLists[ListIdx]);
}

if (ForceNetUpdateReplicationActorList.Num() > 0)
{
Params.OutGatheredReplicationLists.AddReplicationActorList(ForceNetUpdateReplicationActorList);
}
}

其它应用

快速范围查询

显然,ReplicationGraphNode_GridSpatialization2DActorWorld 中按照 Cell 进行了划分;这样的结构可以直接借用来做一些基本的通用需求:比如快速查询某个 Location 一定范围内的所有 Pawn(正常需要对空间额外维护一棵四叉树来处理);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 获取 Player 视野范围内的 Actor

TArray<FActorRepListType> UReplicationGraphNode_GridSpatialization2D::GetActorsByPlayer(const APawn* Player)
{
if (!IsValid(Player)) return {};

TArray<FActorRepListType> Result;

FVector ClampedViewLoc = Player->GetActorLocation();
if (GridBounds.IsValid)
{
ClampedViewLoc = GridBounds.GetClosestPointTo(ClampedViewLoc);
}
else
{
ClampedViewLoc = ClampedViewLoc.BoundToCube(HALF_WORLD_MAX);
}


int32 CellX = UE::LWC::FloatToIntCastChecked<int32>((ClampedViewLoc.X - SpatialBias.X) / CellSize);
if (CellX < 0)
{
CellX = 0;
}

int32 CellY = UE::LWC::FloatToIntCastChecked<int32>((ClampedViewLoc.Y - SpatialBias.Y) / CellSize);
if (CellY < 0)
{
CellY = 0;
}

if (UReplicationGraphNode_GridCell* CellNode = GetCell(GetGridX(CellX), CellY))
{
CellNode->GetAllActorsInNode_Debugging(Result);
}

return Result;
}
1
2
3
4
5
6
7
8
// 获取坐标范围内的 Cell 中的 Actor

void UReplicationGraphNode_GridSpatialization2D::GetGridActorsByLocationAndRange(FActorRepListType Actor, const FVector& Location3D, float Radius, TArray<UReplicationGraphNode_GridCell*>& OutGatheredNodes)
{
const FActorCellInfo& CellInfo = GetCellInfoForActor(Actor, Location3D, Radius);
GetGridNodesForActor(Actor, CellInfo, OutGatheredNodes);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 获取 Position 范围 Radius(cm) 内的 Pawn;如果 bAccurate,则需要严格判定,否则模糊判定在 Cell 内即可;

TArray<APawn*> AOIUtils::GetAllPawnsInPosGrid(UWorld* World, const FVector& Position, float Radius, bool bAccurate)
{
if (!IsValid(World) || !IsValid(World->GetNetDriver())) return {};

auto RepGraph = World->GetNetDriver()->GetReplicationDriver();
if (!IsValid(RepGraph)) return {};

auto GridNode = RepGraph->GridNode;
if (!IsValid(GridNode)) return {};

float RadiusSquared = Radius * Radius;

TArray<UReplicationGraphNode_GridCell*> OutGatheredNodes;
GridNode->GetGridActorsByLocationAndRange(nullptr, Position, Radius, OutGatheredNodes);

TArray<APawn*> Result;
for (const auto Cell : OutGatheredNodes)
{
TArray<TWeakObjectPtr<AActor>>& LocatedActors = Cell->GetLocationNode();
if (LocatedActors.Num() <= 0) continue;

for (auto WeakActorPtr : LocatedActors)
{
auto Pawn = Cast<APawn>(WeakActorPtr);
if (!IsValid(Pawn)) continue;
if (!bAccurate || FVector::DistSquared2D(Pawn->GetActorLocation(), Position) <= RadiusSquared)
{
Result.Add(Pawn);
}
}
}

return Result;
}

参考

UE ReplicationGraph Document

Lyra (UE5.0-5.3) ReplicationGraph 部分源码